/*
 * The MIT License
 *
 * Copyright 2013-2014 Florian Barras.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package jeo.math.calculator.process;

import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import jeo.common.exception.UnknownClassException;
import jeo.common.exception.UnknownTypeException;
import jeo.common.io.IOManager;
import jeo.common.io.Message;
import jeo.common.math.Maths;
import jeo.common.structure.Pair;
import jeo.common.structure.tree.RedBlackTreeMap;
import jeo.common.thread.Report;
import jeo.common.thread.WorkQueue;
import jeo.common.util.Strings;
import jeo.math.calculator.model.BinaryOperation;
import jeo.math.calculator.model.Element;
import jeo.math.calculator.model.Element.Type;
import jeo.math.calculator.model.MatrixElement;
import jeo.math.calculator.model.Result;
import jeo.math.calculator.model.ScalarElement;
import jeo.math.calculator.model.UnaryOperation;
import jeo.math.calculator.thread.TreeEvaluator;
import jeo.math.linearalgebra.Entity;
import jeo.math.linearalgebra.Matrix;
import jeo.math.linearalgebra.Scalar;

public class Calculator
{
	////////////////////////////////////////////////////////////////////////////
	// ATTRIBUTE(S)
	////////////////////////////////////////////////////////////////////////////

	private static final boolean USE_THREADS = false; // false
	private static final WorkQueue<TreeEvaluator, Pair<Element, Map<String, Element>>, Entity> threadPool = new WorkQueue<TreeEvaluator, Pair<Element, Map<String, Element>>, Entity>();
	private final Map<String, Element> context;


	////////////////////////////////////////////////////////////////////////////
	// CONSTRUCTOR(S)
	////////////////////////////////////////////////////////////////////////////

	public Calculator()
	{
		context = new RedBlackTreeMap<String, Element>();
	}


	////////////////////////////////////////////////////////////////////////////
	// CALCULATOR
	////////////////////////////////////////////////////////////////////////////

	/**
	 * Initializes the working threads.
	 */
	public static void start()
	{
		if (USE_THREADS)
		{
			threadPool.initWorkers(new TreeEvaluator(threadPool));
		}
		ExpressionHandler.init();
	}

	/**
	 * Ends the pool of working threads.
	 */
	public static void end()
	{
		ExpressionHandler.end();
		if (USE_THREADS)
		{
			threadPool.end();
		}
	}

	/**
	 * Evaluates the specified tree of operations and numbers.
	 * <p>
	 * @param tree    the input {@code Element} to be evaluated
	 * @param context the context containing the values of the variables
	 * <p>
	 * @return the evaluation of {@code tree}
	 */
	public static Report<Entity> evaluateTree(final Element tree, final Map<String, Element> context)
	{
		if (tree instanceof ScalarElement || tree instanceof MatrixElement)
		{
			final Entity entity = tree.getEntity();
			IOManager.printDebug("COMPUTE Get entity <" + entity + ">");
			return new Report<Entity>(entity, null);
		}
		else
		{
			if (tree instanceof BinaryOperation)
			{
				final BinaryOperation node = (BinaryOperation) tree;
				final Entity subresult;
				final Report<Entity> leftResult, rightResult;
				final Entity leftEntity, rightEntity;
				if (USE_THREADS && threadPool.reserveWorkers(2))
				{
					final Long leftId, rightId;
					// Add the tasks
					leftId = threadPool.addTask(new Pair<Element, Map<String, Element>>(node.getLeft(), context));
					rightId = threadPool.addTask(new Pair<Element, Map<String, Element>>(node.getRight(), context));
					// Get the results
					leftResult = threadPool.getResult(leftId);
					rightResult = threadPool.getResult(rightId);
				}
				else
				{
					// Get the results
					leftResult = evaluateTree(node.getLeft(), context);
					rightResult = evaluateTree(node.getRight(), context);
				}
				// Get the entities from the results
				// - Left
				leftEntity = leftResult.getOutput();
				if (leftEntity == null)
				{
					return new Report<Entity>(null, leftResult.getMessage());
				}
				// - Right
				rightEntity = rightResult.getOutput();
				if (rightEntity == null)
				{
					return new Report<Entity>(null, rightResult.getMessage());
				}
				// Get the type of binary operation
				final Type type = node.getType();
				IOManager.printDebug("COMPUTE " + leftEntity + " " + type + " " + rightEntity);
				// Evaluate the operation
				switch (type)
				{
					case ADDITION:
						subresult = leftEntity.plus(rightEntity);
						break;
					case SUBTRACTION:
						subresult = leftEntity.minus(rightEntity);
						break;
					case MULTIPLICATION:
						subresult = leftEntity.times(rightEntity);
						break;
					case DIVISION:
						subresult = leftEntity.division(rightEntity);
						break;
					case POWER:
						subresult = leftEntity.power(rightEntity);
						break;
					case SOLUTION:
						subresult = leftEntity.solution(rightEntity);
						break;
					default:
						return new Report<Entity>(null, new Message(new UnknownTypeException(type)));
				}
				return new Report<Entity>(subresult, null);
			}
			else
			{
				if (tree instanceof UnaryOperation)
				{
					final Entity subresult;
					final UnaryOperation node = (UnaryOperation) tree;
					// Get the result
					final Report<Entity> result = evaluateTree(node.getElement(), context);
					final Entity entity = result.getOutput();
					if (entity == null)
					{
						return new Report<Entity>(null, result.getMessage());
					}
					// Get the type of unary operation
					final Type type = node.getType();
					IOManager.printDebug("COMPUTE " + type + " " + entity);
					// Evaluate the operation
					switch (type)
					{
						case FACTORIAL:
							final Scalar scalar = (Scalar) entity;
							final BigInteger bigInt = Maths.factorial(scalar.intValue());
							subresult = new Scalar(bigInt.doubleValue());
							break;
						case TRANSPOSE:
							subresult = entity.transpose();
							break;
						case INVERSE:
							subresult = entity.inverse();
							break;
						default:
							return new Report<Entity>(null, new Message(new UnknownTypeException(type)));
					}
					return new Report<Entity>(subresult, null);
				}
				else
				{
					return new Report<Entity>(null, new Message(new UnknownClassException(tree.getClass())));
				}
			}
		}
	}

	/**
	 * Parses the specified expression to a tree of operations and numbers and
	 * evaluates it.
	 * <p>
	 * @param expression the input {@code String} to be parsed
	 * @param context    the context containing the values of the variables
	 * <p>
	 * @return the evaluation of {@code expression}
	 */
	private Report<Entity> evaluate(final String expression, final Map<String, Element> context)
		throws Exception
	{
		final Report<Element> result = ExpressionHandler.parseExpression(expression, context);
		final Element element = result.getOutput();
		if (element == null)
		{
			return new Report<Entity>(null, result.getMessage());
		}
		return evaluateTree(element, context);
	}

	/**
	 * Processes the specified expression (assignment or simple evaluation).
	 * <p>
	 * @param expression the input {@code String} to be parsed
	 * <p>
	 * @return the evaluation of {@code expression}
	 */
	public Result process(final String expression)
	{
		try
		{
			// Trim the expression
			String trimmedExpression = expression.trim();
			// Check if the epression is an assignment
			final List<String> expressions = Strings.split(trimmedExpression, '=');
			final int size = expressions.size();
			// If the expression is an assignment (or several assignments)
			if (size > 1)
			{
				// Extract the right-hand side of the expression
				trimmedExpression = expressions.get(size - 1).trim();
			}
			// Evaluate the (right-hand side) expression
			final Report<Entity> result = evaluate(trimmedExpression, context);
			final Entity entity = result.getOutput();
			if (entity == null)
			{
				return new Result(null, result.getMessage());
			}
			// Get the corresponding element
			final Element element;
			if (entity instanceof Scalar)
			{
				element = new ScalarElement(null, trimmedExpression, (Scalar) entity);
			}
			else if (entity instanceof Matrix)
			{
				element = new MatrixElement(null, trimmedExpression, (Matrix) entity);
			}
			else
			{
				return new Result(null, new Message(new UnknownClassException(entity.getClass())));
			}
			// If the expression is an assignment (or several assignments)
			if (size > 1)
			{
				// Set the corresponding variable(s)
				for (int i = 0; i < size - 1; ++i)
				{
					context.put(expressions.get(i).trim(), element);
				}
			}
			// Return the evaluation of the (right-hand side) expression
			return new Result(entity, null);
		}
		catch (final Exception ex)
		{
			return new Result(null, IOManager.printError(ex));
		}
	}
}
